import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelBinarizer
import matplotlib.pyplot as plt
import time
import matplotlib as mpl  # 添加matplotlib配置

# 设置中文字体支持 - 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'Microsoft YaHei', 'WenQuanYi Micro Hei']  # 添加多个备选字体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
print("已配置中文字体支持")


# 定义激活函数        将线性加权和转换为非线性输出
def sigmod(x):
    return 1 / (1 + np.exp(-x))

#dsigmod是Sigmoid函数的导数，计算隐藏层激活函数的梯度
def dsigmod(x):
    return x * (1 - x)


# 训练模型（添加时间统计和准确率记录）
def train(X, y, steps, lr=0.15):
    global V, W  # 声明使用全局权重矩阵V和W  	W：隐藏层到输出层的权重矩阵
    start_time = time.time()  # 记录训练开始时间，用于计算总耗时

    # 初始化记录列表：存储训练过程中的准确率和对应步数
    accuracies = []  # 记录每次评估的准确率
    step_points = []  # 记录评估点对应的训练步数

    # 主训练循环：执行指定步数的训练
    for n in range(steps + 1):  # 遍历每一步训练（0到steps）
        # 随机选择一个训练样本
        i = np.random.randint(X.shape[0])  # 随机索引
        x = np.atleast_2d(X[i])  # 确保输入为二维数组（1×64）

        # --- 前向传播：计算网络输出 ---
        # 输入层→隐藏层：计算隐藏层激活值
        L1 = sigmod(np.dot(x, V))  # x(1×64) × V(64×100) → (1×100)
        # 隐藏层→输出层：计算输出层预测值      np.dot(L1, W)：计算隐藏层到输出层的加权和
        L2 = sigmod(np.dot(L1, W))  # 隐藏层L1(1×100) × W(100×10) → (1×10)

        # --- 反向传播：计算误差梯度 ---
        # 输出层误差：(真实标签 - 预测值) × 输出层激活函数的导数
        L2_delta = (y[i] - L2) * dsigmod(L2)  # (1×10)
        # 隐藏层误差：(输出层误差 × 权重转置) × 隐藏层激活函数的导数
        L1_delta = L2_delta.dot(W.T) * dsigmod(L1)  # (1×100)

        # --- 权重更新：梯度下降法 ---W：隐藏层到输出层的权重矩阵   	V：输入层到隐藏层的权重矩阵
        # 更新隐藏层→输出层权重矩阵：
        W += lr * L1.T.dot(L2_delta)  # L1.T(100×1) × L2_delta(1×10) → (100×10)
        # 更新输入层→隐藏层权重：学习率乘以梯度，得到权重调整量
        V += lr * x.T.dot(L1_delta)  # x.T(64×1) × L1_delta(1×100) → (64×100)

        # 定期评估模型性能（每100步）
        if n % 100 == 0:  # 每训练100步评估一次
            acc = test(False)  # 在测试集上评估准确率（不输出时间）
            accuracies.append(acc)  # 记录当前准确率
            step_points.append(n)  # 记录当前步数
            # 输出进度：当前步数/总步数 + 准确率
            print(f"步骤 [{n}/{steps}] - 准确率: {acc:.4f}")

    # 训练结束处理
    train_time = time.time() - start_time  # 计算总训练耗时
    print(f"\n训练完成! 总耗时: {train_time:.4f}秒")  # 输出总训练时间
    # 返回训练时间、记录点和对应的准确率（用于绘图）
    return train_time, step_points, accuracies


# 预测函数
def predict(x):
    """
    使用训练好的神经网络模型进行预测
    参数:
        x: 输入数据，可以是单个样本(1×64)或多个样本(n×64)
    返回:
        输出层的预测结果(1×10或n×10)，每个样本对应10个输出神经元的激活值
    """
    # 前向传播：输入层 → 隐藏层
    # 计算隐藏层激活值：输入数据 × 输入层到隐藏层权重
    L1 = sigmod(np.dot(x, V))  # 应用Sigmoid激活函数

    # 前向传播：隐藏层 → 输出层
    # 计算输出层预测值：隐藏层输出 × 隐藏层到输出层权重
    L2 = sigmod(np.dot(L1, W))  # 再次应用Sigmoid激活函数

    return L2  # 返回预测结果至输出层


# 测试函数（添加时间统计）评估准确率
def test(print_time=True):
    """
    评估模型在测试集上的性能
    参数:
        print_time: 是否打印评估耗时（默认打印）
    返回:
        模型在测试集上的准确率
    """
    # 记录评估开始时间
    start_time = time.time()

    # 批量预测整个测试集（高效方式）
    # 计算所有测试样本的隐藏层输出
    L1 = sigmod(np.dot(X_test, V))  # X_test: (n×64), V: (64×100) → (n×100)

    # 计算所有测试样本的输出层预测
    output = sigmod(np.dot(L1, W))  # L1: (n×100), W: (100×10) → (n×10)

    # 获取预测类别：选择输出层中最大激活值对应的索引
    # axis=1 表示对每行(每个样本)取最大值的位置
    predictions = np.argmax(output, axis=1)  # (n×10) → (n×1)

    # 计算准确率：预测正确的样本比例
    # 比较预测类别和真实标签，计算平均值
    acc = np.mean(predictions == y_test)  # 返回0.0-1.0之间的值

    # 计算评估耗时
    eval_time = time.time() - start_time

    # 根据参数决定是否打印评估时间
    if print_time:
        print(f"模型评估时间: {eval_time:.6f}秒")

    return acc  # 返回准确率
#-----------------------main----------------------
# 载入数据
digits = load_digits()
print("数据集形状:", digits.images.shape)

# 显示示例图片
plt.figure(figsize=(10, 4))
for i in range(5):
    plt.subplot(1, 5, i + 1)
    plt.imshow(digits.images[i], cmap='gray')
    plt.axis('off')
    plt.title(f"数字 {digits.target[i]}", fontsize=10)
plt.suptitle('手写数字示例', fontsize=14)
plt.tight_layout()
plt.show()

# 准备数据
X = digits.data
y = digits.target

# 1. 修改训练集测试集比例为7:3
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
print(f"训练集大小: {X_train.shape[0]}, 测试集大小: {X_test.shape[0]}")

# 标签二值化
labels_train = LabelBinarizer().fit_transform(y_train)

# 2. 初始化网络参数
np.random.seed(42)  # 设置随机种子保证可重复性
V = np.random.random((64, 100)) * 2 - 1#输入层到隐藏层的权重矩阵(64×100)
W = np.random.random((100, 10)) * 2 - 1#隐藏层到输出层的权重矩阵(100×10)

# 3. 训练步数设置为500进行训练
print("\n开始训练神经网络...")
train_time, step_points, accuracies = train(X_train, labels_train, 500)  # 3. 训练步数设为500

# 最终评估
print("\n最终模型评估:")
final_acc = test()
print(f"最终测试准确率: {final_acc:.4f}")

# 样本预测展示
print("\n测试样本预测示例:")
sample_indices = np.random.choice(len(X_test), 10, replace=False)
for i in sample_indices:
    prediction = np.argmax(predict(X_test[i]))
    actual = y_test[i]
    status = "✓" if prediction == actual else "✗"
    print(f"样本 {i:3d}: 实际值={actual}, 预测值={prediction} {status}")

# 绘制训练过程
plt.figure(figsize=(10, 5))
plt.plot(step_points, accuracies, marker='o', linestyle='-', color='b')
plt.title('训练过程中准确率变化', fontsize=14)
plt.xlabel('训练步数', fontsize=12)
plt.ylabel('测试准确率', fontsize=12)
plt.grid(True, linestyle='--', alpha=0.7)
plt.ylim(0, 1)
plt.tight_layout()
plt.savefig('accuracy_plot.png', dpi=300)  # 保存图像以防显示问题
plt.show()